﻿// =====================================================================
//
//  This file is part of the Microsoft Dynamics CRM SDK code samples.
//
//  Copyright (C) Microsoft Corporation.  All rights reserved.
//
//  This source code is intended only as a supplement to Microsoft
//  Development Tools and/or on-line documentation.  See these other
//  materials for detailed information regarding Microsoft code samples.
//
//  THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
//
// =====================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Net;
using System.Security;
using System.Text;
using System.Xml.Serialization;

namespace Microsoft.Xrm.Sdk.Client
{
	internal sealed class IdentityProviderDictionary : Dictionary<String, IdentityProvider>
	{
	}

	internal enum IdentityProviderType
	{
		ADFS = 0,
		LiveId,
		OrgId,
	}
	public abstract class IdentityProvider
	{
		public Uri ServiceUrl { get; set; }

		internal IdentityProviderType IdentityProviderType { get; set; }
	}
	public sealed class OnlineIdentityProvider : IdentityProvider
	{

	}
	public sealed class LocalIdentityProvider : IdentityProvider
	{

	}

	[SecuritySafeCritical()]
	internal sealed class IdentityProviderLookup
	{
		private static IdentityProviderLookup _instance;

		private readonly IdentityProviderDictionary _identityProviderDictionary = new IdentityProviderDictionary();

		private const String OrgIdIdentityService = @"{0}/GetUserRealm.srf";

		private const String OrgIdIdentityQuery = @"login={0}&xml=1";

		public static IdentityProviderLookup Instance
		{
			get { return _instance ?? (_instance = new IdentityProviderLookup()); }
		}

		internal IdentityProviderDictionary IdentityProviderDictionary
		{
			get { return _identityProviderDictionary; }
		}

		public static void Clear()
		{
			_instance = null;
		}

		/// <summary>
		/// This lookup returns the service root url for the identity provider associated to the user principal name.
		/// </summary>
		/// <param name="host">The OrgID host to query</param>
		/// <param name="orgIdServiceRoot">The OrgID service root for non ADFS requests</param>
		/// <param name="userPrincipalName">The domainSuffix to look for.  May be empty.</param>
		/// <returns>The service root of the corresponding online provider, or the local ADFS WS-Trust metadata endpoint.</returns>
		[SuppressMessage("Microsoft.Security", "CA9885:AvoidHttpUtilityMethods", Justification = "This code is client code for external use with no platform dependencies.  We can't add those.")]
		public void GetIdentityProviderAsync(Uri host, Uri orgIdServiceRoot, String userPrincipalName)
		{
			ClientExceptionHelper.ThrowIfNull(host, "host");
			ClientExceptionHelper.ThrowIfNull(orgIdServiceRoot, "orgIdServiceRoot");
			ClientExceptionHelper.ThrowIfNullOrEmpty(userPrincipalName, "userPrincipalName");

			var identityProvider = TryCacheLookup(userPrincipalName);
			if (identityProvider != null)
			{
				RaiseIdentityProviderLoadedEvent(identityProvider, null, userPrincipalName);
				return;
            }

#if SILVERLIGHT
			string orgQuery = string.Format(CultureInfo.InvariantCulture, OrgIdIdentityQuery, HttpUtility.UrlEncode(userPrincipalName));
#else
            string orgQuery = string.Format(CultureInfo.InvariantCulture, OrgIdIdentityQuery, Uri.EscapeDataString(userPrincipalName));
#endif

            string serviceEndpoint = string.Format(CultureInfo.InvariantCulture, OrgIdIdentityService, host.AbsoluteUri);

			var getRequest = (HttpWebRequest)WebRequest.Create(new Uri(serviceEndpoint));
			getRequest.Method = "POST";
			getRequest.ContentType = "application/x-www-form-urlencoded";

			var state = new IdentityProviderRequestState()
			{
				Request = getRequest,
				OrgQuery = orgQuery,
				UserPrincipalName = userPrincipalName,
				OrgIdServiceRoot = orgIdServiceRoot
			};

			getRequest.BeginGetRequestStream(BeginGetRequestStreamCallback, state);
		}

		internal class IdentityProviderRequestState
		{
			public WebRequest Request;
			public string OrgQuery;
			public string UserPrincipalName;
			public Uri OrgIdServiceRoot;
		}

		private void BeginGetRequestStreamCallback(IAsyncResult result)
		{
			IdentityProviderRequestState requestState = (IdentityProviderRequestState)result.AsyncState;
			var getRequest = requestState.Request;
			var orgQuery = requestState.OrgQuery;

			using (var requestStream = getRequest.EndGetRequestStream(result))
			{
				byte[] byteArray = Encoding.UTF8.GetBytes(orgQuery);
				// Write to the request stream.
				requestStream.Write(byteArray, 0, orgQuery.Length);
			}
			//Now make the call to issue the device token
			requestState.Request.BeginGetResponse(IdentityProviderCallback, requestState);
		}


		private void IdentityProviderCallback(IAsyncResult result)
		{
							IdentityProviderRequestState requestState = (IdentityProviderRequestState)result.AsyncState;
			try
			{
				RealmInfo realmInfo;

				var getRequest = requestState.Request;
				using (var getResponse = (HttpWebResponse)requestState.Request.EndGetResponse(result))
				{
					using (Stream responseStream = getResponse.GetResponseStream())
					{
						using (var streamReader = new StreamReader(responseStream))
						{
							XmlSerializer serializer = new XmlSerializer(typeof(RealmInfo));

							realmInfo = serializer.Deserialize(streamReader) as RealmInfo;
						}
					}
				}

				if (realmInfo != null)
				{
					IdentityProvider identityProvider = null;
					if (String.IsNullOrEmpty(realmInfo.MetadataUrl))
					{
						identityProvider = new OnlineIdentityProvider() { IdentityProviderType = IdentityProviderType.OrgId, ServiceUrl = requestState.OrgIdServiceRoot };
					}
					else
					{
						identityProvider = new LocalIdentityProvider() { IdentityProviderType = IdentityProviderType.ADFS, ServiceUrl = new Uri(realmInfo.MetadataUrl) };
					}
					InsertIdentityProvider(requestState.UserPrincipalName, identityProvider);

					RaiseIdentityProviderLoadedEvent(identityProvider, requestState, requestState.UserPrincipalName);
				}
			}
			catch (Exception ex)
			{
				//We could attempt to parse out the error here but because it is an HTML response, we would have to write quite a large amount of code to handle it safely.
				//Because this will switch to a real mechanism from ACS at a later point, we will just eat this exception, because the most likely reason is that the realm 
				//has not been configured in ACS.

				identityProvidersLoaded(this, new IdentityProvidersLoadCompletedEventArgs(ex, false, null, null, requestState.UserPrincipalName)); ;
			}
		}

		private IdentityProvider TryCacheLookup(String userPrincipalName)
		{
			lock (this)
			{
				if (IdentityProviderDictionary.ContainsKey(userPrincipalName))
				{
					return IdentityProviderDictionary[userPrincipalName];
				}
				else
				{
					return null;
				}
			}
		}

		private void InsertIdentityProvider(string userPrincipalName, IdentityProvider identityProvider)
		{
			lock (this)
			{
				IdentityProviderDictionary[userPrincipalName] = identityProvider;
			}
		}

		private void RaiseIdentityProviderLoadedEvent(IdentityProvider identityProvider, IdentityProviderRequestState state, string userPrincipalName)
		{
			identityProvidersLoaded(this, new IdentityProvidersLoadCompletedEventArgs(null, false, state, identityProvider, userPrincipalName));
		}

		public EventHandler<IdentityProvidersLoadCompletedEventArgs> identityProvidersLoaded;

	}
	/// <summary>
	/// Must be public for de-serialization
	/// </summary>
	public sealed class RealmInfo
	{
		public String State { get; set; }
		public String UserState { get; set; }
		public String LogOn { get; set; }
		public String FederationGlobalVersion { get; set; }
		public String DomainName { get; set; }
		[SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Uri is not XmlSerializable, and the name of the property is dictated by the spec", MessageId = "")]
		[XmlElement("AuthURL")]
		public String AuthorizationUrl { get; set; }
		[XmlElement("IsFederatedNS")]
		public Boolean IsFederatedNamespace { get; set; }
		[SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Uri is not XmlSerializable, and the name of the property is dictated by the spec", MessageId = "")]
		[XmlElement("STSAuthURL")]
		public String TokenServiceAuthenticationUrl { get; set; }
		public int FederationTier { get; set; }
		public String FederationBrandName { get; set; }
		[XmlElement("AllowFedUsersWLIDSignIn")]
		public Boolean AllowFedUsersLiveIdSignIn { get; set; }
		public String Certificate { get; set; }
		[SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Uri is not XmlSerializable, and the name of the property is dictated by the spec", MessageId = "")]
		[XmlElement("MEXURL")]
		public String MetadataUrl { get; set; }
		[SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Uri is not XmlSerializable, and the name of the property is dictated by the spec", MessageId = "")]
		[XmlElement("SAML_AuthURL")]
		public String SamlAuthUrl { get; set; }
		public int PreferredProtocol { get; set; }

		[XmlIgnore]
		[XmlElement("EDUDomainFlags")]
		public int EduDomains { get; set; }
	}

	public class IdentityProvidersLoadCompletedEventArgs : AsyncCompletedEventArgs
	{
		public IdentityProvidersLoadCompletedEventArgs()
			: base(null, false, null)
		{
		}

		public IdentityProvidersLoadCompletedEventArgs(Exception error, bool cancelled, object userState, IdentityProvider result, string userName)
			: base(error, cancelled, userState)
		{
			this.result = result;
			this.userPrincipalName = userName;
		}

		IdentityProvider result;
		string userPrincipalName;

		public string UserPrincipalName
		{
			get { return userPrincipalName; }
		}

		public IdentityProvider Result
		{
			get
			{
				base.RaiseExceptionIfNecessary();
				return this.result;
			}

			set
			{
				this.result = value;
			}
		}
	}
}
